Skip to content

How to create forms with Server Actions

React Server Actions are Server Functions that execute on the server. They can be called in Server and Client Components to handle form submissions. This guide will walk you through how to create forms in Next.js with Server Actions.

How it works

React extends the HTML <form> element to allow Server Actions to be invoked with the action attribute.

When used in a form, the function automatically receives the FormData object. You can then extract the data using the native FormData methods:

app/invoices/page.tsx
export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
 
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
 
    // mutate data
    // revalidate the cache
  }
 
  return <form action={createInvoice}>...</form>
}

Good to know: When working with forms that have multiple fields, you can use the entries() method with JavaScript's Object.fromEntries(). For example: const rawFormData = Object.fromEntries(formData).

Passing additional arguments

Outside of form fields, you can pass additional arguments to a Server Function using the JavaScript bind method. For example, to pass the userId argument to the updateUser Server Function:

app/client-component.tsx
'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

The Server Function will receive the userId as an additional argument:

app/actions.ts
'use server'
 
export async function updateUser(userId: string, formData: FormData) {}

Good to know:

  • An alternative is to pass arguments as hidden input fields in the form (e.g. <input type="hidden" name="userId" value={userId} />). However, the value will be part of the rendered HTML and will not be encoded.
  • bind works in both Server and Client Components and supports progressive enhancement.

Form validation

Forms can be validate on the client or server.

  • For client-side validation, you can use the HTML attributes like required and type="email" for basic validation.
  • For server-side validation, you can use a library like zod to validate the form fields. For example:
app/actions.ts
'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // Return early if the form data is invalid
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Mutate data
}

Validation errors

To display validation errors or messages, turn the component that defines the <form> into a Client Component and use React useActionState.

When using useActionState, the Server function signature will change to receive a new prevState or initialState parameter as its first argument.

app/actions.ts
'use server'
 
import { z } from 'zod'
 
export async function createUser(initialState: any, formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}

You can then conditionally render the error message based on the state object.

app/ui/signup.tsx
'use client'
 
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button disabled={pending}>Sign up</button>
    </form>
  )
}

Pending states

The useActionState hook exposes a pending boolean that can be used to show a loading indicator or disable the submit button while the action is being executed.

app/ui/signup.tsx
'use client'
 
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
 
export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)
 
  return (
    <form action={formAction}>
      {/* Other form elements */}
      <button disabled={pending}>Sign up</button>
    </form>
  )
}

Alternatively, you can use the useFormStatus hook to show a loading indicator while the action is being executed. When using this hook, you'll need to create a separate component to render the loading indicator. For example, to disable the button when the action is pending:

app/ui/button.tsx
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Sign Up
    </button>
  )
}

You can then nest the SubmitButton component inside the form:

app/ui/signup.tsx
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
 
export function Signup() {
  return (
    <form action={createUser}>
      {/* Other form elements */}
      <SubmitButton />
    </form>
  )
}

Good to know: In React 19, useFormStatus includes additional keys on the returned object, like data, method, and action. If you are not using React 19, only the pending key is available.

Optimistic updates

You can use the React useOptimistic hook to optimistically update the UI before the Server Function finishes executing, rather than waiting for the response:

app/page.tsx
'use client'
 
import { useOptimistic } from 'react'
import { send } from './actions'
 
type Message = {
  message: string
}
 
export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<
    Message[],
    string
  >(messages, (state, newMessage) => [...state, { message: newMessage }])
 
  const formAction = async (formData: FormData) => {
    const message = formData.get('message') as string
    addOptimisticMessage(message)
    await send(message)
  }
 
  return (
    <div>
      {optimisticMessages.map((m, i) => (
        <div key={i}>{m.message}</div>
      ))}
      <form action={formAction}>
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

Nested form elements

You can call Server Actions in elements nested inside <form> such as <button>, <input type="submit">, and <input type="image">. These elements accept the formAction prop or event handlers.

This is useful in cases where you want to call multiple Server Actions within a form. For example, you can create a specific <button> element for saving a post draft in addition to publishing it. See the React <form> docs for more information.

Programmatic form submission

You can trigger a form submission programmatically using the requestSubmit() method. For example, when the user submits a form using the + Enter keyboard shortcut, you can listen for the onKeyDown event:

app/entry.tsx
'use client'
 
export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }
 
  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

This will trigger the submission of the nearest <form> ancestor, which will invoke the Server Function.